训练营——机器学习L1系列

机器学习

对蘑菇进行聚类分析

提示:这样的文字将会指导你如何使用 jupyter Notebook 来完成项目。你可以通过单击代码区域,然后使用键盘快捷键 Shift+Enter 或 Shift+Return 来运行代码。或者在选择代码后使用执行(run cell)按钮执行代码。

在如下有# TODO 提示的地方,将代码补全,实现注释中所要求的功能。

在有" 回答:" 提示的地方,回答其上所提出的问题。(Markdown的文字区域也同样可以双击然后编辑和执行

开始

在本项目中,我们将在蘑菇属性数据集上尝试运用机器学习模型Kmeans,依据数据的特征构建蘑菇分群。

我们尝试使用不断探索的方式来分析这份数据并建立机器学习模型。这个工程的结构如下:

  • 初探:我们使用最简单的方式处理数据,看看模型的效果会是如何的。
  • 进一步探索:我们希望在这部分更进一步,对数据进行降维处理,看看模型的效果会不会被大幅削弱。
  • 展示:我们会在这一部分将聚类的效果以非常直观的方式展示出来,让大家对聚类算法有更进一步理解。

读入数据

运行下面的代码将数据读入。

In [1]:
# 导入常用库
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

# 读取csv文件并存储到dataframe里面
data = pd.read_csv('./mushrooms.csv')
data.head()
Out[1]:
cap-shape cap-surface cap-color bruises odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-surface-below-ring stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat
0 x s n t p f c n k e ... s w w p w o p k s u
1 x s y t a f c b k e ... s w w p w o p n n g
2 b s w t l f c b n e ... s w w p w o p n n m
3 x y w t p f c n n e ... s w w p w o p k s u
4 x s g f n f w b k t ... s w w p w o e n a g

5 rows × 22 columns

观察

从蘑菇数据样本中,我们可以看到数据的一些特征:

  • cap-shape: 帽型,铃形=b,锥形=c,凸=x,平面=f,旋钮=k,下陷=s
  • cap-surface: 帽面,纤维=f,沟槽=g,鳞=y,光滑=s
  • cap-color: 帽色,褐色=n,黄色=b,肉桂=c,灰色=g,绿色=r,粉红色=p,紫色=u,红色=e,白色=w,黄色=y
  • bruises: 瘀伤,瘀伤=t,不=f
  • odor: 气味,杏仁=a,茴香=1,杂酚油=c,鱼=y,臭味=f,霉变=m,无一=n,辛辣=p,辛辣=s。
  • gill-attachment: 褶附着,附件=a,下降=d,自由=f,缺口=n
  • gill-spacing: 褶间距,密=c,挤=w,远=d
  • gill-size: 褶大小,宽=b,窄=n
  • gill-color: 褶色,黑色=k,棕色=n,黄色=b,巧克力=h,灰色=g,绿色=r,橙色=o,粉红色=p,紫色=u,红色=e,白色=w,黄色=y。
  • stalk-shape: 柄形,放大=e,变细=t
  • stalk-root: 茎-根,球根=b,棍棒=c,杯=u,相等=e,根状茎=z,根=r,缺失=?
  • stalk-surface-above-ring: 环上茎表面,纤维=f,鳞=y,丝=k,光滑=s
  • stalk-surface-below-ring: 环下茎表面:纤维=f,鳞=y,丝=k,光滑=s
  • stalk-color-above-ring: 环上茎色:褐色=n,BUff=b,肉桂=c,灰色=g,橙色=o,粉红色=p,红色=e,白色=w,黄色=y
  • stalk-color-below-ring: 环下茎色:褐色=n,黄色=b,肉桂=c,灰色=g,橙色=o,粉红色=p,红色=e,白色=w,黄色=y
  • veil-type: 覆盖物型,部分=p,通用=u
  • veil-color: 覆盖物颜色,褐色=n,橙色=o,白色=w,黄色=y
  • ring-number: 环数,0=n,1=o,2=t
  • ring-type: 圆环型:cobwebby=c,式样=e,flling=f,大=l,none=n,吊坠=p,护套=s,zone=z。
  • spore-print-color: 孢子印色,黑色=k,棕色=n,黄色=b,巧克力=h,绿色=r,橙色=o,紫色=u,白色=w,黄色=y
  • population: 种群,富足=a,丛集=c,众多=n,分散=s,数=v,孤立=y
  • habitat: 生长环境:草=g,叶=1,草甸=m,小径=p,城市=u,废物=w,森林=d

特种中包含帽型、帽面、帽色等植物学相关的特征,我们这里并不需要研究植物学之后再对这些数据进行处理,我们先将这些特征都作为数据的不同维度属性来平等对待。

探索

现在就让我们来观察一下数据中每一列的数字分布情况吧!

In [2]:
import matplotlib.pyplot as plt
from math import ceil
import seaborn as sns

%matplotlib inline

font = {'family' : 'Times New Roman',
              'weight' : 'normal',
              'size'   : 23}

plt.figure(figsize=(30, 150))
# 将要绘制图形的每行由多少个直方图组成
column_per_line = 3
# 属性的总数
columns = len(data.columns)
n_row = ceil(columns/column_per_line)

for i, attr in enumerate(data.columns):
    # TODO
    # 绘制columns个直方图(每个属性一个图),描绘每个属性中不同值的分布情况。直方图最好绘制成三列的排布方式,方便在如下结果窗中展示。
    # 可以使用如上定义的font字体让显示的字体更清楚些。
    
    #数据简单处理
    data_to_plt = data[attr].value_counts()
    #画图
    plt.subplot(n_row, column_per_line, i+1)
    data_to_plt.plot.bar()
    plt.title(attr, fontdict=font)

plt.show()
/home/may/anaconda3/lib/python3.7/site-packages/matplotlib/font_manager.py:1241: UserWarning: findfont: Font family ['Times New Roman'] not found. Falling back to DejaVu Sans.
  (prop.get_family(), self.defaultFamily[fontext]))

从结果中,可以看到,哪一列属性只有一个值,不具备区分度?

回答: 不具备区分度的列是 veil-type

In [3]:
# TODO
# 从data中将这列没有区分度的属性去除掉
"""画不出图,直接判断好了"""
for col in data.columns:
    if len(data[col].unique()) == 1:
        print('属性只有一列的是:', col)
        col_k = col
        break

data = data.drop(col_k, axis=1)
data.shape
属性只有一列的是: veil-type
Out[3]:
(8124, 21)

编码

这个案例中所有数据属性都是非数值类型的,所以我们需要对属性值进行编码处理。

我们将对仅包含2个可能值的变量使用一个简单的二进制编码,对包含3个或多个可能值的变量进行热编码。

In [4]:
# 使用sklearn中的LabelEncoder方法对仅有两个可能值的变量进行编码操作
for col in data.columns:
    # TODO
    # 判断col这列是否只有两个值,如果是,使用LabelEncoder对其进行转换并更新到data相应列中
    if len(data[col].unique()) == 2:
        label_enc = LabelEncoder()
        data[col] = label_enc.fit_transform(data[col])
                
# 打印转换之后的数据头部
data.head()
Out[4]:
cap-shape cap-surface cap-color bruises odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-surface-above-ring stalk-surface-below-ring stalk-color-above-ring stalk-color-below-ring veil-color ring-number ring-type spore-print-color population habitat
0 x s n 1 p 1 0 1 k 0 ... s s w w w o p k s u
1 x s y 1 a 1 0 0 k 0 ... s s w w w o p n n g
2 b s w 1 l 1 0 0 n 0 ... s s w w w o p n n m
3 x y w 1 p 1 0 1 n 0 ... s s w w w o p k s u
4 x s g 0 n 1 1 0 k 1 ... s s w w w o e n a g

5 rows × 21 columns

对于多值属性,标签编码以及独热编码两个步骤,在Pandas当中,我们可以直接使用get_dummies一步完成:

In [5]:
# 使用pandas中的get_dummies方法对属性进行独热编码
data = pd.get_dummies(data)
data.head()
Out[5]:
bruises gill-attachment gill-spacing gill-size stalk-shape cap-shape_b cap-shape_c cap-shape_f cap-shape_k cap-shape_s ... population_s population_v population_y habitat_d habitat_g habitat_l habitat_m habitat_p habitat_u habitat_w
0 1 1 0 1 0 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 1 0
1 1 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 1 0 0 0 0 0
2 1 1 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 1 0 0 0
3 1 1 0 1 0 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 1 0
4 0 1 1 0 1 0 0 0 0 0 ... 0 0 0 0 1 0 0 0 0 0

5 rows × 111 columns

初步探索

  • 我们用K-means模型来对数据进行拟合。
  • 我们使用轮廓系数对模型进行评价。
  • 这里我们将遍历不同的k值获取模型的最优参数。
In [6]:
import time
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans

def cluster(X):
    '''
    K-Means聚类执行函数。
    
    输入
        X:待处理的数据。
    输出
        best_clusters:分数(轮廓系数)最高的分类器
        score:分类器在数据上的轮廓系数
        
    '''
    score = -1
    duration = 0
    start = time.time()
    # 取K值为2到15
    for clusters_num in range(2, 15):  
        # TODO
        # 建立kmeans模型,其中聚类数等于clusters_num
        kmeans = KMeans(n_clusters=clusters_num)
        
        # TODO
        # 使用kmeans模型对数据X进行拟合,并用该模型对数据X做聚类预测,存储到test_preds变量中
        kmeans.fit(X)
        test_preds = kmeans.predict(X)

        # TODO
        # 使用silhouette_score计算当前模型预测所得的轮廓系数并赋值到变量test_score中
        test_score = silhouette_score(X, test_preds)
        
        # 打印计算的轮廓系数
        print("component score is ", test_score)

        #将效果最好的模型中的参数保存起来
        if (score < test_score):
            best_clustersnum = clusters_num
            score = test_score
            best_clusters=kmeans

    # 统计时间
    end = time.time()      
    duration += end - start

    # 打印结果
    print('训练模型耗时: {:.4f}s'.format(duration))
    print('当k为{}的时候,轮廓系数最高,为{:.3f}'.format(best_clustersnum, score))
    
    return best_clusters, score

# 调用cluster函数对data数据做聚类处理
clu, score = cluster(data)
component score is  0.1578442723522316
component score is  0.20502879374121666
component score is  0.21043413105310402
component score is  0.2335300576808903
component score is  0.24885156982880144
component score is  0.25263702856910786
component score is  0.2566889363486038
component score is  0.26571681248307805
component score is  0.21796169585196534
component score is  0.21659916684007646
component score is  0.23210233463636865
component score is  0.14290946016378697
component score is  0.14844074179300212
训练模型耗时: 40.0683s
当k为9的时候,轮廓系数最高,为0.266

进一步探索

降维处理

PCA(principal Component Analysis),即主成分分析方法,是一种使用最广泛的数据压缩算法。

PCA将原有的数据特征进行坐标转换,生成新的特征坐标。我们可以指定PCA转换所希望得到的最终特征数来对原始数据进行这样的处理。

PCA算法可以报告出生成的每一个主成分的解释方差比(explained variance ratio)——数据中有多少方差能够用这个单独的维度来解释。

这里需要特别留意的是,PCA生成的新维度并不是将原有的某些数据维度删除所得,而是由原来数据中的特征转换而来的。

In [7]:
from sklearn.decomposition import PCA
# TODO
# 声明维度为20的PCA对象
pca = PCA(n_components=20)

# 对data进行拟合
pca.fit(data)

# pca.components_中记录了按照解释方差比从大到小排列主成分
# 使用柱形图绘制每个主成分与原数据特征之间的关系
components = pd.DataFrame(np.round(pca.components_, 4), columns = [data.keys()])
components.plot.bar(figsize=(20, 6))
Out[7]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f35e83cb278>

由于原数据经过独热编码处理之后,维度达到一百多,以上图形看起来并不是那么美观。不过并不影响我们理解:经过PCA转换之后返回的pca.components_表示按照方差解释比从大到小排列的数据的主成分。我们从图中看到,每个主成分都与原有的数据特征之间存在一定的关系。

我们不妨多做一点点研究,尝试绘制一下20个主成分与前两个维度(bruises、gill-attachment)之间的关系。

In [8]:
# TODO
# 使用类似如上图形绘制的方法,绘制20个主成分与前两个维度(bruises、gill-attachment)之间的关系
components_f2 = components.iloc[:, :2]
components_f2.plot.bar(figsize=(20, 6))
Out[8]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f35eaf6c518>

问题:从结果图中分析一下,在所以PCA的主成分(component)中,编号为多少的component与bruises的相关性最大。

注意:每个原特征与主成分的相关性具有正和负两个不同的方向,当这里看到的相关性值绝对值越大,表示原属性与主成分之间的关系越大。参考:https://onlinecourses.science.psu.edu/stat505/node/54/

回答: 0

接下来,我们计算一下pca中各主成分的方差解释比,以及pca转化之后所有的主成分所能解释的原数据的方差解释比之和。

In [9]:
# TODO
# 取出pca转换之后各主成分的方差解释比,存在变量var_ratio中
# var_ratio中的元素从大到小排列,分别表示排名最靠前一直到最靠后的方差解释比值各是多少。
var_ratio = -np.sort(-pca.explained_variance_ratio_)

# 打印pca各component的方差解释比
print(var_ratio)

# TODO
# 打印所有方差解释比之和
print(sum(var_ratio))
[0.1619248  0.1372911  0.09241893 0.05522428 0.04015055 0.0382942
 0.03330125 0.02881895 0.02336357 0.02235037 0.02209855 0.01945468
 0.01931061 0.01704709 0.01622855 0.01593556 0.01561185 0.01505545
 0.01338132 0.01209798]
0.7993596430722862

这里我们期望做个探索,当选取最少的前多少个主成分时,方差解释比就可以达到70%以上。

In [10]:
# TODO
# 逐个计算var_ratio中的各元素之和,当至少取前多少个元素时,和大于0.7
var_ratio_sum=0
for i in range(len(var_ratio)):
    if var_ratio_sum <= 0.7:
        var_ratio_sum += var_ratio[i]
    else:
        print('选取最少前%s个时,可以达到70%%以上'% i)
        break
选取最少前14个时,可以达到70%以上

问题:当选取最少的前多少个主成分时,方差解释比就可以达到70%以上?

回答: 14

按照这个成分数量定义,我们重新做下PCA转换:

In [11]:
# TODO
# 重新定义PCA变量pca,主成分数量定义为上面计算的使得解释方差比之和大于0.7的最少特征数。
pca = PCA(n_components=14)

# TODO
# 使用pca对数据data进行拟合
pca.fit(data)
Out[11]:
PCA(copy=True, iterated_power='auto', n_components=14, random_state=None,
  svd_solver='auto', tol=0.0, whiten=False)

现在我们来看一看,降维之后模型的效果怎么样吧!

In [12]:
# 使用pca对data进行转换并重新做聚类计算
data_pca = pca.transform(data)
clu, score = cluster(data_pca)
component score is  0.22483337818222576
component score is  0.2996030074666612
component score is  0.3149358439198902
component score is  0.34524524335564594
component score is  0.3697945364015664
component score is  0.3833643422895598
component score is  0.3771007619814482
component score is  0.3517963736635071
component score is  0.3211189915641628
component score is  0.300593782402121
component score is  0.2521983947541402
component score is  0.26383847943682437
component score is  0.2481028805675258
训练模型耗时: 26.4179s
当k为7的时候,轮廓系数最高,为0.383

问题 : 在降维后,由于维度变小节省了计算时间,模型的分数(轮廓系数)是否受到了不好的影响?

回答: 没有,甚至有所提高


聚类效果绘制

现在我们就将聚类的效果通过散点图的形式展示出来,其中,点的颜色表明它属于的簇,数字代表该簇的簇内中心点,这些中心(或者叫平均点)并不是数据中真实存在的点,但是是所有预测在这个簇中的数据点的平均。

为了方便观察,我们只挑选前两个主成分作为坐标轴,绘制二维的散点图。

In [13]:
import visuals as vs

# 使用刚才最优化模型对data_pca数据进行预测,结果存到变量preds中
preds = clu.predict(data_pca)

# TODO
# 从聚类模型cluster中获取各聚类中心点,结果存到变量centers中
centers = clu.cluster_centers_

# 绘制散点图
vs.cluster_results(data_pca,preds,centers)

揭晓

在这个案例中,我们所处理的数据其实少掉了一个列属性。任务开始之前,我们有意将它删除了。

真正完整的数据,放在文件./mushrooms_o.csv当中,让我尝试加载一下:

In [14]:
# 加载并打印mushrooms_o.csv中的数据
data_full = pd.read_csv('./mushrooms_o.csv')
data_full.head()
Out[14]:
class cap-shape cap-surface cap-color bruises odor gill-attachment gill-spacing gill-size gill-color ... stalk-surface-below-ring stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat
0 p x s n t p f c n k ... s w w p w o p k s u
1 e x s y t a f c b k ... s w w p w o p n n g
2 e b s w t l f c b n ... s w w p w o p n n m
3 p x y w t p f c n n ... s w w p w o p k s u
4 e x s g f n f w b k ... s w w p w o e n a g

5 rows × 23 columns

这份数据中的class这一列,在任务开始之初被我们拿掉了。

这列其实在原始数据中,表示了蘑菇所属的分类,p表示有毒、e表示可以食用。

In [15]:
from sklearn.preprocessing import LabelEncoder

# TODO
# 将class这列从data_full中提取出来,并用LableEncoder做编码转换,转码之后结果存在变量class_label中
encoder = LabelEncoder()
class_label = encoder.fit_transform(data_full['class'])

# 输出一下class_label的前3个元素
print(class_label[:3])
[1 0 0]

问题:通过LabelEncoder编码之后,p和e所对应的编码分别是什么?

回答: e--1, p--0

In [16]:
# 绘制原始数据在pca前两个主成分坐标系中的分类图
vs.real_results(data_pca,class_label)

虽然在建立KMeans模型之初我们并不知道数据中存在分类,但通过聚类之后,我们会发现所聚出的簇跟蘑菇是否有毒的分类存在有趣的相关性。

问题:在原始数据的有毒蘑菇中,从图中可以大致对应到我们通过KMeans聚出来的那几个簇?

回答: 0, 2, 6


小补充:

数据通过聚类算法处理获取的聚类特征,在实际项目中可以作为原有特征的一个附加特征列加入到数据中。

这样之后,将重新组合出来的数据作为监督学习(比如分类)输入数据,往往可以获取到比只使用原有特征进行计算更好的结果。


选做

在设置PCA主成分数量的时候,我们在以上操作中通过循环尝试选择了能保留70%多方差解释比的主成分数量,在最终的聚类与原始数据的分类分布上还存在一些差异。

在构建PCA对象的时候,传入的n_components参数如果值为一个小于1的小数,将表示在PCA转换中,保留多大比例的方差解释比。

在以下代码中,尝试定义一个保留百分之九十九(0.99)方差解释比的PCA,并用它对数据进行处理,最后同样绘制在前两个主成分坐标系中的散点图,对比聚类效果与原始数据的分类状况的近似情况。

In [17]:
# TODO

import visuals as vs

# 构建保留百分之九十九方差解释比的PCA并对data进行拟合转换
# 然后使用转换之后的数据训练一个KMeans模型,并绘制数据在PCA转换之后在前两个主成分坐标上的聚类分布散点图
pca_99 = PCA(n_components=0.99)
pca_99.fit(data)

data_pca_99 = pca.transform(data)
clu, score = cluster(data_pca_99)

preds_99 = clu.predict(data_pca_99)

centers = clu.cluster_centers_

# 绘制散点图
vs.cluster_results(data_pca_99, preds, centers)
component score is  0.22483337818222576
component score is  0.2996030074666612
component score is  0.3149358439198902
component score is  0.34524524335564594
component score is  0.3697945364015664
component score is  0.3832985070248769
component score is  0.36284556543400065
component score is  0.32170947531005867
component score is  0.3458570453528695
component score is  0.30098552716016386
component score is  0.2600087596351242
component score is  0.26339975930052284
component score is  0.25849002496734735
训练模型耗时: 31.3937s
当k为7的时候,轮廓系数最高,为0.383
In [ ]: